package sf.r2dbc.rowmapper;

import io.r2dbc.spi.ColumnMetadata;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import sf.core.DBObject;
import sf.database.listener.EntityListenerManager;
import sf.database.meta.ColumnMapping;
import sf.database.meta.MetaHolder;
import sf.database.meta.TableMapping;
import sf.database.util.OrmValueUtils;
import sf.database.util.SQLUtils;
import sf.r2dbc.sql.R2dbcConvertUtils;
import sf.spring.util.LinkedCaseInsensitiveMap;
import sf.tools.ArrayUtils;
import sf.tools.StringUtils;

import javax.persistence.ColumnResult;
import javax.persistence.ConstructorResult;
import javax.persistence.EntityResult;
import javax.persistence.FieldResult;
import javax.persistence.SqlResultSetMapping;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

/**
 * SqlResultSetMapping对象转换:
 */
public class R2dbcJPABeanRowMapper implements R2dbcRowMapper<Object> {
    private Function<Object, Object> function;
    private boolean useTail;
    private boolean keepUpdate;//是否保留update字段值
    private SqlResultSetMapping resultSetMapping;


    /**
     * @param resultSetMapping
     */
    public R2dbcJPABeanRowMapper(SqlResultSetMapping resultSetMapping) {
        this.resultSetMapping = resultSetMapping;
    }

    /**
     * @param resultSetMapping JPA注解
     * @param useTail          是否使用tail
     * @param keepUpdate       是否保留DBObject中的update字段值
     * @param function         回调方法
     */
    public R2dbcJPABeanRowMapper(SqlResultSetMapping resultSetMapping, boolean useTail, boolean keepUpdate, Function<Object, Object> function) {
        this.useTail = useTail;
        this.keepUpdate = keepUpdate;
        this.resultSetMapping = resultSetMapping;
        this.function = function;
    }

    @Override
    public Object apply(Row rs, RowMetadata rsmd) {
        try {
            EntityResult[] entityResults = resultSetMapping.entities();
            ConstructorResult[] constructorResults = resultSetMapping.classes();
            List<Object> resultObjs = new ArrayList<>();
            if (ArrayUtils.isNotEmpty(entityResults)) {
                for (EntityResult entityResult : entityResults) {
                    Object obj = convertToBean(rs, rsmd, entityResult.entityClass(), entityResult.fields());
                    EntityListenerManager.runPostLoad(obj);
                    resultObjs.add(obj);
                }
            }
            if (ArrayUtils.isNotEmpty(constructorResults)) {
                for (ConstructorResult constructorResult : constructorResults) {
                    Object obj = constructorToBean(rs, constructorResult.targetClass(), constructorResult.columns());
                    EntityListenerManager.runPostLoad(obj);
                    resultObjs.add(obj);
                }
            }

            if (resultObjs.size() == 1) {
                return resultObjs.get(0);
            }
            if (resultObjs.size() > 1) {
                return resultObjs.toArray();
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException("Can't set bean property.", e);
        }
    }

    /**
     * 构造方法创建对象
     * @param rs
     * @param beanClass
     * @param fieldResults
     * @return
     * @throws SQLException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws java.lang.reflect.InvocationTargetException
     */
    private Object constructorToBean(Row rs, Class<?> beanClass, ColumnResult[] fieldResults) throws InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException {
        Constructor<?>[] allConstructors = beanClass.getDeclaredConstructors();
        Constructor<?> constructor = null;
        for (Constructor<?> con : allConstructors) {
            boolean classEQ = true;
            if (con.getParameterCount() == fieldResults.length) {
                for (int i = 0; i < fieldResults.length; i++) {
                    if (fieldResults[i].type() != void.class) {
                        if (!Objects.equals(fieldResults[i].type(), con.getParameters()[i].getType())) {
                            classEQ = false;
                        }
                    }
                }
            } else {
                classEQ = false;
            }
            if (classEQ) {
                constructor = con;
                break;
            }
        }
        if (constructor != null) {
            Parameter[] parameters = constructor.getParameters();
            Object[] valueObjects = new Object[parameters.length];
            for (int i = 0; i < fieldResults.length; i++) {
                String column = fieldResults[i].name();
                valueObjects[i] = R2dbcConvertUtils.get(rs.get(column), null, parameters[i].getType(), null, null);
            }
            Object bean = constructor.newInstance(valueObjects);
            if (function != null) {
                bean = function.apply(bean);
            }
            return bean;
        }
        return null;
    }

    /**
     * 转换普通对象
     * @param rs
     * @param rsmd
     * @param beanClass
     * @param fieldResults
     * @return
     * @throws SQLException
     */
    private Object convertToBean(Row rs, RowMetadata rsmd, Class<?> beanClass, FieldResult[] fieldResults) throws SQLException {
        TableMapping table = MetaHolder.getMeta(beanClass);
        Object bean = OrmValueUtils.instance(table);
        boolean isDBObject = DBObject.class.isAssignableFrom(beanClass);
        if (isDBObject && !keepUpdate) {
            DBObject d = (DBObject) bean;
            //关闭动态插值
            d.stopUpdate();
        }
        for (ColumnMetadata cmd : rsmd.getColumnMetadatas()) {
            String columnName = cmd.getName();
            ColumnMapping cm = SQLUtils.getColumnByDBName(table, columnName);
            if (cm != null) {
                Object value = R2dbcConvertUtils.get(rs.get(columnName), null, cm);
                if (/*!cm.getClz().isPrimitive() || */value != null) {
                    //不为基础类型或者 值不为空,则插值
                    OrmValueUtils.setValue(bean, cm, value);
                }
                stackFind(rs, fieldResults, bean, columnName);
                continue;
            }
            boolean find = stackFind(rs, fieldResults, bean, columnName);
            if (!find && useTail) {
                //未找到将值设置在tail中.
                if (isDBObject) {
                    DBObject d = (DBObject) bean;
                    Map<String, Object> map = d.getTail();
                    if (map == null) {
                        map = new LinkedCaseInsensitiveMap<>();
                        d.setTail(map);
                    }
                    map.put(columnName, rs.get(columnName));
                } else if (table.getTailField() != null) {
                    Map<String, Object> map = OrmValueUtils.getValue(bean, table.getTailField());
                    if (map == null) {
                        map = new LinkedCaseInsensitiveMap<>();
                        OrmValueUtils.setValue(bean, table.getTailField(), map);
                    }
                    map.put(columnName, rs.get(columnName));
                }
            }
        }
        if (isDBObject && !keepUpdate) {
            DBObject d = (DBObject) bean;
//                d.clearUpdate();
            //开启动态插值
            d.startUpdate();
        }
        if (function != null) {
            bean = function.apply(bean);
        }
        return bean;
    }

    /**
     * 迭代查找赋值
     * @param rs
     * @param fieldResults
     * @param bean
     * @param columnName
     * @return
     * @throws SQLException
     */
    private boolean stackFind(Row rs, FieldResult[] fieldResults, Object bean, String columnName)
            throws SQLException {
        boolean flag = false;
        for (int i = 0; i < fieldResults.length; i++) {
            String fieldName = fieldResults[i].name();
            String column = fieldResults[i].column();
            if (columnName.equals(column)) {
                String[] fieldNames = StringUtils.split(fieldName, ".");
                flag = setSubProperties(bean, fieldNames, 0, rs, columnName);
            }
        }
        return flag;
    }

    private boolean setSubProperties(Object bean, String[] fieldPaths, int i, Row rs, String columnName) throws SQLException {
        boolean flag = false;
        String fieldName = fieldPaths[i];
        TableMapping table = MetaHolder.getMeta(bean.getClass());
        ColumnMapping cm = table.getJavaFieldColumnMapping(fieldName);
        if (cm != null) {
            if (fieldPaths.length == 1 || i == fieldPaths.length - 1) {
                Object value = R2dbcConvertUtils.get(rs.get(columnName), null, cm);
                if (value != null) {
                    OrmValueUtils.setValue(bean, cm, value);
                }
                flag = true;
            } else {
                Object obj = null;
                Object innerObj = OrmValueUtils.getValue(bean, cm);
                if (innerObj == null) {
                    obj = OrmValueUtils.instance(MetaHolder.getMeta(cm.getClz()));
                } else {
                    obj = innerObj;
                }
                boolean isDBObject = DBObject.class.isAssignableFrom(cm.getClz());
                if (isDBObject) {
                    DBObject d = (DBObject) obj;
                    //关闭动态插值
                    d.stopUpdate();
                }
                flag = setSubProperties(obj, fieldPaths, i + 1, rs, columnName);
                // 使用set字段方法,非method,可能对某些特殊方法执行不到
                if (innerObj == null) {
                    OrmValueUtils.setValue(bean, cm, obj);
                }
                //清除updateValue
                if (isDBObject) {
                    DBObject d = (DBObject) obj;
                    //开启动态插值
                    d.startUpdate();
                }
            }
        }
        return flag;
    }

}
